#include "myopenglwidget.h"
#include <iostream>
#include <drm_fourcc.h>
#include "my_shader.h"

#define GL_CHECK(stmt) stmt; GLCheckError(#stmt, __FILE__, __LINE__);

static void GLCheckError(const char* stmt, const char* filename, const int line)
{
    GLenum err = glGetError();
    if (err != GL_NO_ERROR) {
        printf("GL error after %s, ret: %d in %d\n", stmt, err, line);
        return;
    }
}


const char* GetColourSpaceText(const boost::optional<MppFrameColorSpace>& mpp_colour_space)
{
  if (!mpp_colour_space.is_initialized())
  {
    return "Unknown";
  }
  std::vector<std::pair<MppFrameColorSpace, std::string>>::const_iterator i = std::find_if(MPP_COLOUR_SPACES.cbegin(), MPP_COLOUR_SPACES.cend(), [&mpp_colour_space](const std::pair<MppFrameColorSpace, std::string>& i){ return (i.first == *mpp_colour_space); });
  if (i == MPP_COLOUR_SPACES.cend())
  {
    return "Unknown";
  }
  return i->second.c_str();
}

const char* GetColourRangeText(const boost::optional<MppFrameColorRange>& mpp_colour_range)
{
  if (!mpp_colour_range.is_initialized())
  {
    return "Unknown";
  }
  std::vector<std::pair<MppFrameColorRange, std::string>>::const_iterator i = std::find_if(MPP_COLOUR_RANGES.cbegin(), MPP_COLOUR_RANGES.cend(), [&mpp_colour_range](const std::pair<MppFrameColorRange, std::string>& i){ return (i.first == *mpp_colour_range); });
  if (i == MPP_COLOUR_RANGES.cend())
  {
    return "Unknown";
  }
  return i->second.c_str();
}

const char* GetColourPrimariesText(const boost::optional<MppFrameColorPrimaries>& mpp_colour_primaries)
{
  if (!mpp_colour_primaries.is_initialized())
  {
    return "Unknown";
  }
  std::vector<std::pair<MppFrameColorPrimaries, std::string>>::const_iterator i = std::find_if(MPP_COLOUR_PRIMARIES.cbegin(), MPP_COLOUR_PRIMARIES.cend(), [&mpp_colour_primaries](const std::pair<MppFrameColorPrimaries, std::string>& i){ return (i.first == *mpp_colour_primaries); });
  if (i == MPP_COLOUR_PRIMARIES.cend())
  {
    return "Unknown";
  }
  return i->second.c_str();
}

MyOpenGLWidget::MyOpenGLWidget(QWidget *parent)
    : QOpenGLWidget(parent)
    , m_decoder(new Decoder)
{
	setWindowFlags(Qt::FramelessWindowHint);
    init_egl_functions();
    m_timer.setTimerType(Qt::PreciseTimer);
    connect(&m_timer, &QTimer::timeout, this, &MyOpenGLWidget::on_timeout);
}

MyOpenGLWidget::~MyOpenGLWidget()
{
}

void MyOpenGLWidget::set_mp4_path(const std::string &path)
{
    m_decoder->set_mp4_path(path);
}

void MyOpenGLWidget::start()
{
    m_decoder->start();
    m_timer.start(16);
}

void MyOpenGLWidget::on_timeout()
{
    if (!m_decoder->send_frame()) {
        m_flag = false;
        m_timer.setInterval(1);
        std::cout << "send false" << std::endl;
//        repaint();
//        update();
        return;
    }
    m_timer.setInterval(16);

    FrameStruct frame;
    if (m_decoder->receive_frame(frame)) {
        m_frame = frame;
        m_flag = true;
    }
    update();
}

void MyOpenGLWidget::init_egl_functions()
{
    egl_create_image_khr = reinterpret_cast<PFNEGLCREATEIMAGEKHRPROC>(eglGetProcAddress("eglCreateImageKHR"));
    egl_destroy_image_khr = reinterpret_cast<PFNEGLDESTROYIMAGEKHRPROC>(eglGetProcAddress("eglDestroyImageKHR"));
    egl_create_sync_khr = reinterpret_cast<PFNEGLCREATESYNCKHRPROC>(eglGetProcAddress("eglCreateSyncKHR"));
    egl_destroy_sync_khr = reinterpret_cast<PFNEGLDESTROYSYNCKHRPROC>(eglGetProcAddress("eglDestroySyncKHR"));
    egl_client_wait_sync_khr = reinterpret_cast<PFNEGLCLIENTWAITSYNCKHRPROC>(eglGetProcAddress("eglClientWaitSyncKHR"));
    gl_egl_image_target_texture_2_does = reinterpret_cast<PFNGLEGLIMAGETARGETTEXTURE2DOESPROC>(eglGetProcAddress("glEGLImageTargetTexture2DOES"));
    if ((egl_create_image_khr == nullptr) || (egl_destroy_image_khr == nullptr) || (egl_create_sync_khr == nullptr) || (egl_destroy_sync_khr == nullptr)
            || (egl_client_wait_sync_khr == nullptr) || (gl_egl_image_target_texture_2_does == nullptr)) {
      std::cout << "Failed to retrieve EGL functions" << std::endl;
      exit(1);
    }
}

void MyOpenGLWidget::do_egl_image(const FrameStruct &frame)
{
//    std::cout << "now in do_egl_image" << std::endl;
    m_mpp_colour_space = frame.mpp_colour_space;
    m_mpp_colour_range = frame.mpp_colour_range;
    m_mpp_colour_primaries = frame.mpp_colour_primaries;

    std::map<MppBuffer, EGL_FRAME>::iterator e = m_egl_images.find(frame.mpp_buffer);
    if (e != m_egl_images.end()) {
        if ((e->second.colour_space_ != frame.mpp_colour_space) || (e->second.colour_range_ != frame.mpp_colour_range)
                || (e->second.width_ != frame.width) || (e->second.height_ != frame.height)) {
            std::cout << "MPP buffer format changed, resetting EGL images" << std::endl;
            destroy_egl_frames();
            e = m_egl_images.end();
        }
    }

    if (e == m_egl_images.end()) {
      const int egl_colour_space = EGL_COLOUR_SPACES[m_egl_colour_space_override_index].first;
      const int egl_colour_range = EGL_COLOUR_RANGES[m_egl_colour_range_override_index].first;
      // Create EGL image
      EGLint atts[] = {
                        EGL_WIDTH, static_cast<EGLint>(frame.width),
                        EGL_HEIGHT, static_cast<EGLint>(frame.height),
                        EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_NV12,
                        EGL_DMA_BUF_PLANE0_FD_EXT, frame.fd,
                        EGL_DMA_BUF_PLANE0_OFFSET_EXT, static_cast<EGLint>(frame.offset_x),
                        EGL_DMA_BUF_PLANE0_PITCH_EXT, static_cast<EGLint>(frame.hor_stride),
                        EGL_DMA_BUF_PLANE1_FD_EXT, frame.fd,
                        EGL_DMA_BUF_PLANE1_OFFSET_EXT, static_cast<EGLint>(frame.offset_x + (frame.hor_stride * frame.ver_stride)),
                        EGL_DMA_BUF_PLANE1_PITCH_EXT, static_cast<EGLint>(frame.hor_stride),
                        EGL_YUV_COLOR_SPACE_HINT_EXT, egl_colour_space,
                        EGL_SAMPLE_RANGE_HINT_EXT, egl_colour_range,
                        EGL_NONE
                      };
      const EGLImageKHR egl_image = egl_create_image_khr(m_egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, atts);
      if (egl_image == EGL_NO_IMAGE_KHR) {
        std::cout << "Failed to create EGL image" << std::endl;
        exit(1);
      }
//        std::cout << "succeed to create EGL image" << std::endl;
      e = m_egl_images.insert(std::make_pair(frame.mpp_buffer, EGL_FRAME(egl_image, frame.mpp_colour_space, frame.mpp_colour_range,
                                                                         frame.width, frame.height))).first;
    }

    // Create and/or frame buffer
    if (m_frame_buffer == nullptr) {
        GLuint frm = GL_INVALID_VALUE;
        glGenFramebuffers(1, &frm);
        glBindFramebuffer(GL_FRAMEBUFFER, frm);
        GLuint frame_buffer_texture = GL_INVALID_VALUE;
        glGenTextures(1, &frame_buffer_texture);
        glBindTexture(GL_TEXTURE_2D, frame_buffer_texture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, frame.width, frame.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, frame_buffer_texture, 0);
        if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
            std::cout << "Failed to create frame buffer" << std::endl;
            exit(1);
        }
        m_frame_buffer = std::make_unique<FRAME_BUFFER>(frm, frame_buffer_texture, frame.width, frame.height);
        printf("bybobbi: first!!!!!!!!, frame.width is %d, frame.height is %d\n", frame.width, frame.height);
    } else {
        glBindFramebuffer(GL_FRAMEBUFFER, m_frame_buffer->frame_);
//        printf("bybobbi: second!!!!!!!!, frame.width is %d, frame.height is %d\n", frame.width, frame.height);
    }

    // Draw the EGL buffer
//    GL_CHECK(m_oes_program.bind());
//      GL_CHECK(glUseProgram(m_oes_program->programId()));
#if 0
      GLuint id = GL_CHECK(m_oes_program->programId());
      std::cout << "id is " << id << std::endl;
      GL_CHECK(glUseProgram(id));
#else
    bool f = GL_CHECK(m_oes_program->bind());
    if (!f) {
        exit(1);
    }
#endif
    // Textures
    GL_CHECK(glActiveTexture(GL_TEXTURE0));
    GL_CHECK(glUniform1i(m_oes_texture_sampler_location, 0));
    GL_CHECK(gl_egl_image_target_texture_2_does(GL_TEXTURE_EXTERNAL_OES, e->second.image_));
    // Draw elements
    GL_CHECK(glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
    GL_CHECK(glClear(GL_COLOR_BUFFER_BIT));
    GL_CHECK(m_vao->bind());
    GL_CHECK(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0));
    // Cleanup
    GL_CHECK(m_vao->release());
    GL_CHECK(glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0));
    GL_CHECK(glBindTexture(GL_TEXTURE_2D, 0));
//    GL_CHECK(glUseProgram(0));
    m_oes_program->release();
    GL_CHECK(glBindFramebuffer(GL_FRAMEBUFFER, 0));

    // Sync
    const EGLSyncKHR egl_sync = egl_create_sync_khr(m_egl_display, EGL_SYNC_FENCE_KHR, nullptr);
    if (egl_sync == EGL_NO_SYNC_KHR) {
      std::cout << "Failed to create EGL sync object" << std::endl;
    } else {
      if (egl_client_wait_sync_khr(m_egl_display, egl_sync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER_KHR)
              != EGL_CONDITION_SATISFIED_KHR) {
        std::cout << "Failed to sync EGL buffers" << std::endl;
      }
      if (egl_destroy_sync_khr(m_egl_display, egl_sync) == EGL_FALSE) {
        std::cout << "Failed to destroy EGL sync object" << std::endl;
      }
    }
}

void MyOpenGLWidget::destroy_egl_frames()
{
    for (auto& egl_image : m_egl_images) {
        if (egl_destroy_image_khr(m_egl_display, egl_image.second.image_) != EGL_TRUE) {
            std::cout << "Failed to destroy EGL image" << std::endl;
        }
    }
    m_egl_images.clear();
}

void MyOpenGLWidget::initializeGL()
{
    printf("now in initializeGL\n");
    initializeOpenGLFunctions();

//    m_program.addShaderFromSourceCode(QOpenGLShader::Vertex, vertex_shader.c_str());
//    m_program.addShaderFromSourceCode(QOpenGLShader::Fragment, fragment_shader.c_str());
//    m_oes_program.addShaderFromSourceCode(QOpenGLShader::Vertex, vertex_shader.c_str());
//    m_oes_program.addShaderFromSourceCode(QOpenGLShader::Fragment, oes_fragment_shader.c_str());
    delete m_program;
    m_program = new QOpenGLShaderProgram;
    GL_CHECK(m_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertex_shader));
    GL_CHECK(m_program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragment_shader));
    delete m_oes_program;
    m_oes_program = new QOpenGLShaderProgram;
    GL_CHECK(m_oes_program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertex_shader));
    GL_CHECK(m_oes_program->addShaderFromSourceCode(QOpenGLShader::Fragment, oes_fragment_shader));

    // Bind attributes
    int position_location = 0;
    int texture_coord_location = 1;
    GL_CHECK(m_program->bindAttributeLocation("position", position_location));
    GL_CHECK(m_program->bindAttributeLocation("texcoord", texture_coord_location));
    GL_CHECK(m_oes_program->bindAttributeLocation("position", position_location));
    GL_CHECK(m_oes_program->bindAttributeLocation("texcoord", texture_coord_location));
    //link
    if (!m_oes_program->link())
        exit(1);
    if (!m_program->link())
        exit(1);


    delete m_vao;
    m_vao = new QOpenGLVertexArrayObject;
    m_vao->create();
    delete m_vbo;
    m_vbo = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
    m_vbo->create();
    delete m_ebo;
    m_ebo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer);
    m_ebo->create();

    m_vao->bind();
    m_vbo->bind();
    m_ebo->bind();

    static const float vertices[] = {
      // Positions  // Texture coords
      1.0f,  1.0f,  1.0f, 1.0f, // top right
      1.0f,  -1.0f, 1.0f, 0.0f, // bottom right
      -1.0f, -1.0f, 0.0f, 0.0f, // bottom left
      -1.0f, 1.0f,  0.0f, 1.0f  // top left
    };
    m_vbo->setUsagePattern(QOpenGLBuffer::StaticDraw);
    m_vbo->allocate(vertices, sizeof(vertices));


    static const unsigned int indices[] =
    {
      0, 1, 3,
      1, 2, 3
    };
    m_ebo->setUsagePattern(QOpenGLBuffer::StaticDraw);
    m_ebo->allocate(indices, sizeof(indices));


    // Position attribute
    glVertexAttribPointer(position_location, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast<void*>(0));
    glEnableVertexAttribArray(position_location);
    // Texture coord attribute
    glVertexAttribPointer(texture_coord_location, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast<void*>(2 * sizeof(float)));
    glEnableVertexAttribArray(texture_coord_location);

    m_vao->release();
    m_ebo->release();
    m_vbo->release();
    GL_CHECK(glDisableVertexAttribArray(position_location));
    GL_CHECK(glDisableVertexAttribArray(texture_coord_location));


    // Retrieve uniforms
    m_oes_texture_sampler_location = m_oes_program->uniformLocation("tex");
    if (m_oes_texture_sampler_location == -1) {
      std::cout << "Failed to retrieve OES texture sampler location" << std::endl;
      exit(1);
    }
    m_texture_sampler_location = m_program->uniformLocation("tex");
    if (m_texture_sampler_location == -1) {
      std::cout << "Failed to retrieve texture sampler location" << std::endl;
      exit(1);
    }

#if 1
    auto eglIface = QOpenGLContext::currentContext()->nativeInterface<QNativeInterface::QEGLContext>();
    m_egl_display = eglIface->display();
#else
    m_egl_display = eglGetCurrentDisplay();
#endif
}

void MyOpenGLWidget::resizeGL(int w, int h)
{
    glViewport(0, 0, w, h);
}

void MyOpenGLWidget::paintGL()
{
#if 0
//    std::cout << "paintGL" << std::endl;
    static bool flag = false;
    if (!m_decoder->send_frame()) {
        QTimer::singleShot(0, [&]() { update();});
        return;
    }

    FrameStruct frame;
    if (m_decoder->receive_frame(frame)) {
        do_egl_image(frame);
        flag = true;
    }
#endif

    if (m_flag) {
        do_egl_image(m_frame);
    }

//    if (!flag) {
//        QTimer::singleShot(0, [&]() { update();});
//        return;
//    }
    // Clear
//    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    GL_CHECK(glClearColor(0.0f, 0.0f, 0.0f, 0.0f));
    GL_CHECK(glClear(GL_COLOR_BUFFER_BIT));
    // Draw video
    if (m_frame_buffer) {
//        std::cout << "frame_buffer is not null" << std::endl;
      // Draw the EGL buffer
      GL_CHECK(m_oes_program->bind());
//      GLuint id = GL_CHECK(m_oes_program->programId());
//      GL_CHECK(glUseProgram(id));
      // Textures
      GL_CHECK(glActiveTexture(GL_TEXTURE0));
      glUniform1i(m_texture_sampler_location, 0);
      GL_CHECK(glBindTexture(GL_TEXTURE_2D, m_frame_buffer->texture_));
      // Draw elements
      m_vao->bind();
      GL_CHECK(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0));
      // Cleanup
      m_vao->release();
      glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
      glBindTexture(GL_TEXTURE_2D, 0);
//      glUseProgram(0);
      m_oes_program->release();
      glBindFramebuffer(GL_FRAMEBUFFER, 0);
    }
    destroy_egl_frames();
//    QTimer::singleShot(10, [&]() { update();});
//    update();
}

